package com.gitee.wsl.time.schedule.cron

import com.gitee.wsl.ext.base.currentDateTime
import kotlinx.datetime.LocalDateTime

typealias Minutes = Int

internal const val millisecondsDefault: Short = 0
internal val millisecondsArrayDefault: Array<Short> = arrayOf(millisecondsDefault)

internal val dayOfWeekRange = 0 .. 6
internal val yearRange = Int.MIN_VALUE .. Int.MAX_VALUE
internal val monthRange = 0 .. 11
internal val dayOfMonthRange = 0 .. 30
internal val hoursRange = 0 .. 23
internal val minutesRange = 0 .. 59
internal val secondsRange = minutesRange
internal val millisecondsRange = 0 .. 999

/**
 * @param daysOfWeek 0-6
 * @param years any int
 * @param months 0-11
 * @param daysOfMonth 0-30
 * @param hours 0-23
 * @param minutes 0-59
 * @param seconds 0-59
 */
data class CronDateTime(
    val daysOfWeek: Array<Byte>? = null,
    val years: Array<Int>? = null,
    val months: Array<Byte>? = null,
    val daysOfMonth: Array<Byte>? = null,
    val hours: Array<Byte>? = null,
    val minutes: Array<Byte>? = null,
    val seconds: Array<Byte>? = null,
    val milliseconds: Array<Short>? = millisecondsArrayDefault
) {
    init {
        check(daysOfWeek ?.all { it in dayOfWeekRange } ?: true)
        check(years?.all { it in yearRange } ?: true)
        check(months?.all { it in monthRange } ?: true)
        check(daysOfMonth ?.all { it in dayOfMonthRange } ?: true)
        check(hours?.all { it in hoursRange } ?: true)
        check(minutes?.all { it in minutesRange } ?: true)
        check(seconds?.all { it in secondsRange } ?: true)
        check(milliseconds?.all { it in millisecondsRange } ?: true)
    }

    internal val calculators = listOf(
        years ?.let { NearDateTimeCalculatorYears(it) },
        daysOfWeek ?.let { NearDateTimeCalculatorWeekDays(it) },
        milliseconds ?.let { NearDateTimeCalculatorMillis(it) },
        seconds ?.let { NearDateTimeCalculatorSeconds(it) },
        minutes ?.let { NearDateTimeCalculatorMinutes(it) },
        hours ?.let { NearDateTimeCalculatorHours(it) },
        daysOfMonth ?.let { NearDateTimeCalculatorDays(it) },
        months ?.let { NearDateTimeCalculatorMonths(it) },
    )

    internal fun toNearDateTime(relativelyTo: LocalDateTime = currentDateTime): LocalDateTime? {
        var current = relativelyTo
        whileLoop@while (true) {
            for (calculator in calculators) {
                val (calculated, requireRecalculation) = (calculator ?: continue).calculateNearTime(current) ?: return null
                current = calculated
                if (requireRecalculation) {
                    continue@whileLoop
                }
            }
            return current
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is CronDateTime) return false

        if (daysOfWeek != null) {
            if (other.daysOfWeek == null) return false
            if (!daysOfWeek.contentEquals(other.daysOfWeek)) return false
        } else if (other.daysOfWeek != null) return false
        if (years != null) {
            if (other.years == null) return false
            if (!years.contentEquals(other.years)) return false
        } else if (other.years != null) return false
        if (months != null) {
            if (other.months == null) return false
            if (!months.contentEquals(other.months)) return false
        } else if (other.months != null) return false
        if (daysOfMonth != null) {
            if (other.daysOfMonth == null) return false
            if (!daysOfMonth.contentEquals(other.daysOfMonth)) return false
        } else if (other.daysOfMonth != null) return false
        if (hours != null) {
            if (other.hours == null) return false
            if (!hours.contentEquals(other.hours)) return false
        } else if (other.hours != null) return false
        if (minutes != null) {
            if (other.minutes == null) return false
            if (!minutes.contentEquals(other.minutes)) return false
        } else if (other.minutes != null) return false
        if (seconds != null) {
            if (other.seconds == null) return false
            if (!seconds.contentEquals(other.seconds)) return false
        } else if (other.seconds != null) return false
        if (milliseconds != null) {
            if (other.milliseconds == null) return false
            if (!milliseconds.contentEquals(other.milliseconds)) return false
        } else if (other.milliseconds != null) return false
        return calculators == other.calculators
    }

    override fun hashCode(): Int {
        var result = daysOfWeek?.contentHashCode() ?: 0
        result = 31 * result + (years?.contentHashCode() ?: 0)
        result = 31 * result + (months?.contentHashCode() ?: 0)
        result = 31 * result + (daysOfMonth?.contentHashCode() ?: 0)
        result = 31 * result + (hours?.contentHashCode() ?: 0)
        result = 31 * result + (minutes?.contentHashCode() ?: 0)
        result = 31 * result + (seconds?.contentHashCode() ?: 0)
        result = 31 * result + (milliseconds?.contentHashCode() ?: 0)
        result = 31 * result + calculators.hashCode()
        return result
    }
}

internal fun createCronDateTime(
    seconds: Array<Byte>? = null,
    minutes: Array<Byte>? = null,
    hours: Array<Byte>? = null,
    dayOfMonth: Array<Byte>? = null,
    month: Array<Byte>? = null,
    years: Array<Int>? = null,
    weekDays: Array<Byte>? = null,
    milliseconds: Array<Short>? = millisecondsArrayDefault
): CronDateTime {
    return CronDateTime(weekDays, years, month, dayOfMonth, hours, minutes, seconds, milliseconds)
}

/**
 * @return [KronScheduler] (in fact [CronDateTimeScheduler]) based on incoming data
 */
internal fun createKronScheduler(
    seconds: Array<Byte>? = null,
    minutes: Array<Byte>? = null,
    hours: Array<Byte>? = null,
    dayOfMonth: Array<Byte>? = null,
    month: Array<Byte>? = null,
    years: Array<Int>? = null,
    weekDays: Array<Byte>? = null,
    milliseconds: Array<Short>? = millisecondsArrayDefault
) = CronDateTimeScheduler(
    createCronDateTime(
        seconds,
        minutes,
        hours,
        dayOfMonth,
        month,
        years,
        weekDays,
        milliseconds
    )
)

internal fun List<CronDateTime>.merge() = CronDateTime(
    flatMap { it.daysOfWeek ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.years ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.months ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.daysOfMonth ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.hours ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.minutes ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.seconds ?.toList() ?: emptyList() }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
    flatMap { it.milliseconds ?.toList() ?: listOf(0) }.distinct().toTypedArray().takeIf { it.isNotEmpty() },
)